package gui.batches;

import gui.commands.*;
import gui.common.Controller;
import gui.common.IView;
import gui.inventory.ProductContainerData;
import gui.item.ItemData;
import gui.product.ProductData;

import java.awt.Desktop;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;

import model.entity.Item;
import model.entity.Product;
import model.entity.container.ProductContainer;
import model.management.ItemManager;
import model.management.ProductManager;
import reports.BarcodeSheet;

import common.Barcode;
import common.Pair;
import common.Result;
import common.util.DateUtils;

/**
 * Controller class for the add item batch view.
 */
public class AddItemBatchController extends Controller implements IAddItemBatchController
{
  private final ProductContainer          container;

  private List<ProductData>               products = new ArrayList<ProductData>();
  private Map<ProductData, Set<ItemData>> items    = new HashMap<ProductData, Set<ItemData>>();

  private Timer                           timer;
  private TimerTask                       task;

  private CommandHistory                  history;

  /**
   * Constructor.
   * 
   * @param view
   *          Reference to the add item batch view.
   * @param target
   *          Reference to the storage unit to which items are being added.
   */
  public AddItemBatchController(IView view, ProductContainerData target)
  {
    super(view);

    container = target.getTag();
    timer = new Timer();
    task = getScannerResponseTask();
    history = new CommandHistory();

    construct();
  }

  /**
   * Returns a reference to the view for this controller.
   */
  @Override
  protected IAddItemBatchView getView()
  {
    return (IAddItemBatchView) super.getView();
  }

  /**
   * Loads data into the controller's view.
   * 
   * {@pre None}
   * 
   * {@post The controller has loaded data into its view}
   */
  @Override
  protected void loadValues()
  {
    getView().setCount("1");
    getView().setEntryDate(DateUtils.currentDate());
    getView().setBarcode("");
    getView().setUseScanner(true);
    getView().giveBarcodeFocus();
  }

  /**
   * Sets the enable/disable state of all components in the controller's view. A component should
   * be enabled only if the user is currently allowed to interact with that component.
   * 
   * {@pre None}
   * 
   * {@post The enable/disable state of all components in the controller's view have been set
   * appropriately.}
   */
  @Override
  protected void enableComponents()
  {
    boolean scanner = getView().getUseScanner();
    boolean enable = canAdd() && !scanner;
    getView().enableItemAction(enable);

    getView().enableUndo(history.canUndo());
    getView().enableRedo(history.canRedo());
  }

  private boolean canAdd()
  {
    boolean countValid = getCount(false) > 0;
    String barcode = getView().getBarcode();
    boolean barcodeValid = barcode != null && !barcode.isEmpty();
    boolean dateValid = getView().getEntryDate() != null
        && getView().getEntryDate().before(new Date());

    return countValid && barcodeValid && dateValid;
  }

  /**
   * This method is called when the "Entry Date" field in the add item batch view is changed by the
   * user.
   */
  @Override
  public void entryDateChanged()
  {
    enableComponents();
  }

  /**
   * This method is called when the "Count" field in the add item batch view is changed by the
   * user.
   */
  @Override
  public void countChanged()
  {
    enableComponents();
  }

  /**
   * This method is called when the "Product Barcode" field in the add item batch view is changed
   * by the user.
   */
  @Override
  public void barcodeChanged()
  {
    if (!getView().getUseScanner())
      enableComponents();
    else
    {
      if (task != null)
        task.cancel();

      timer.schedule(getScannerResponseTask(), 200);
    }
  }

  /**
   * This method is called when the "Use Barcode Scanner" setting in the add item batch view is
   * changed by the user.
   */
  @Override
  public void useScannerChanged()
  {
    enableComponents();
  }

  /**
   * This method is called when the selected product changes in the add item batch view.
   */
  @Override
  public void selectedProductChanged()
  {
    ProductData data = getView().getSelectedProduct();
    updateItems(data);
  }

  /**
   * This method is called when the user clicks the "Add Item" button in the add item batch view.
   */
  @Override
  public void addItem()
  {
    int count = getCount(true);
    if (count <= 0)
      return;

    @SuppressWarnings("unchecked")
    Pair<Item, AddProductCommand>[] items = new Pair[count];
    for (int i = 0; i < count; i++)
    {
      Pair<Item, AddProductCommand> pair = getNewItem();
      if (pair == null)
      {
        // user cancelled process
        loadValues();
        enableComponents();
        return;
      }
      items[i] = pair;
    }

    Result result = ItemManager.validateAddItem(items[0].first);
    if (result.getStatus())
      itemsAdded(items);
    else
      getView().displayErrorMessage("Cannot add item:\n" + result.getMessage());

    loadValues();
    enableComponents();
  }

  private int getCount(boolean alert)
  {
    try
    {
      return Integer.parseInt(getView().getCount());
    }
    catch (NumberFormatException e)
    {
      if (alert)
        getView().displayErrorMessage("Please enter a valid number in the Count field");

      return 0;
    }
  }

  private Pair<Item, AddProductCommand> getNewItem()
  {
    Pair<Product, AddProductCommand> pair = getProduct();
    if (pair == null)
      // user cancelled process
      return null;

    Product product = pair.first;
    AddProductCommand c = pair.second;

    if (product == null)
      return null;
    Date entryDate = getView().getEntryDate();
    if (entryDate == null)
      entryDate = DateUtils.currentDate();
    Date expDate = DateUtils.addMonthsToDate(entryDate, product.getShelfLife());
    Barcode barcode = new Barcode();

    return new Pair<Item, AddProductCommand>(new Item(product, barcode, entryDate, expDate,
        container), c);
  }

  private Pair<Product, AddProductCommand> getProduct()
  {
    AddProductCommand c = null;
    String code = getView().getBarcode().trim();
    Product product = ProductManager.getProduct(code);
    if (product == null)
    {
      getView().displayAddProductView();
      product = ProductManager.getProduct(code);
      if (product == null)
        // user cancelled process
        return null;

      c = new AddProductCommand(product, container);
    }

    if (product != null)
      product.addToProductContainer(container);

    return new Pair<Product, AddProductCommand>(product, c);
  }

  private void itemsAdded(Pair<Item, AddProductCommand>[] items)
  {
    MultipleCommands commands = new MultipleCommands();

    for (Pair<Item, AddProductCommand> pair : items)
    {
      Item item = pair.first;
      AddProductCommand c = pair.second;

      if (c != null)
        commands.addCommand(c);

      commands.addCommand(new AddItemCommand(item));

      ItemManager.addItem(item);
    }

    ProductData product = items[0].first.getProduct().generateProductDataObject(
        items[0].first.getContainer());
    if (!products.contains(product))
    {
      DisplayProductCommand c = new DisplayProductCommand(product);
      c.execute();
      commands.addCommand(c);
    }

    if (this.items.get(product) == null)
      this.items.put(product, new TreeSet<ItemData>(ItemManager.getItemSorterByDate()));

    for (Pair<Item, AddProductCommand> pair : items)
    {
      Item item = pair.first;
      ItemData itemData = item.generateItemDataObject();
      Command c = new DisplayItemCommand(itemData, product);
      commands.addCommand(c);
      c.execute();
    }

    history.add(commands);
    updateView(product);
  }

  private void updateView(ProductData product)
  {
    updateProducts();
    updateItems(product);
    getView().selectProduct(product);
  }

  private void updateProducts()
  {
    ProductData[] products = this.products.toArray(new ProductData[0]);
    for (int i = 0; i < products.length; i++)
      products[i].setCount(String.valueOf(items.get(products[i]).size()));

    getView().setProducts(products);
  }

  private void updateItems(ProductData product)
  {
    Set<ItemData> items = this.items.get(product);
    ItemData[] itemData = items.toArray(new ItemData[items.size()]);
    getView().setItems(itemData);
  }

  /**
   * This method is called when the user clicks the "Redo" button in the add item batch view.
   */
  @Override
  public void redo()
  {
    history.redo();
    enableComponents();
  }

  /**
   * This method is called when the user clicks the "Undo" button in the add item batch view.
   */
  @Override
  public void undo()
  {
    history.undo();
    enableComponents();
  }

  /**
   * This method is called when the user clicks the "Done" button in the add item batch view.
   */
  @Override
  public void done()
  {
    if (items.size() > 0)
    {
      BarcodeSheet sheet = new BarcodeSheet();

      for (Set<ItemData> id : items.values())
      {
        for (ItemData itemData : id)
        {
          Item item = itemData.getTag();
          sheet.addItem(item);
        }
      }

      try
      {
        Path path = Files.createTempFile(null, ".pdf");
        sheet.createPDF(path);
        Desktop.getDesktop().open(path.toFile());
      }
      catch (Exception e)
      {}
    }

    getView().close();
  }

  private TimerTask getScannerResponseTask()
  {
    return task = new TimerTask()
    {
      @Override
      public void run()
      {
        if (canAdd())
          addItem();
      }
    };
  }

  private class DisplayItemCommand implements Command
  {
    private final ItemData    itemData;
    private final ProductData productData;

    public DisplayItemCommand(ItemData itemData, ProductData productData)
    {
      this.itemData = itemData;
      this.productData = productData;
    }

    @Override
    public Result reverse()
    {
      items.get(productData).remove(itemData);
      updateView(productData);
      return new Result(true);
    }

    @Override
    public Result execute()
    {
      items.get(productData).add(itemData);
      updateView(productData);
      return new Result(true);
    }

  }

  private class DisplayProductCommand implements Command
  {
    private final ProductData product;

    public DisplayProductCommand(ProductData product)
    {
      this.product = product;
    }

    @Override
    public Result reverse()
    {
      products.remove(product);
      updateView(product);
      return new Result(true);
    }

    @Override
    public Result execute()
    {
      products.add(product);
      return new Result(true);
    }

  }
}
